#include "rendPlatform.hpp"
#include "rendSystem.hpp"
#include "rendMesh.hpp"
#include "rendVisual.hpp"
#include "rendLight.hpp"
#include "rendBoxVisual.hpp"
#include "rendCameraVisual.hpp"
#include "rendSkeleton.hpp"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <memory.h>
#include <time.h>
#include <new>

static unsigned window_w = 768;
static unsigned window_h = 768;

static const char argEscapePrefix		= '-';
static const char argFrequency[]		= "timer_freq";
static const char argNumFrames[]		= "frames";
static const char argDiscardSmooth[]	= "discard_smoothing";
static const char argInvertCull[]		= "invert_cull";
static const char argOtherUp[]			= "other_up";
static const char argFullscreen[]		= "screen";
static const char argNoMiniView[]		= "no_mini_view";
static const char argModelOffset[]		= "model_offset";

static float freq, rfreq;
static unsigned frames;
static const char *fullscreen;
static bool mini_view = true;
static bool grab_frame;

static const float rateY = 4.f;					// rate of movement of camera/target along the y-axis, per input

static float cameraY;							// position of camera along the y-axis
static float targetY;							// position of camera target along the y-axis
static float radius = 128.f;					// radius of camera rotation in the (x, z) plane

static const float rateA = 2.f * M_PI / 60.f;	// rate of camera rotation, per second

static float angle_step = rateA;
static float anim_speed = .125f;
static unsigned max_shininess = 256;


static void
saveFramebuffer()
{
	const char nameBase[] = "frame";
	const char nameExt[] = ".raw";

	const unsigned pixels_len = 4 * window_h * window_w;
	GLvoid * const pixels = malloc(pixels_len);

	glReadPixels(0, 0, window_w, window_h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

	const time_t t = time(NULL);
	const tm* tt = localtime(&t);

	char name[FILENAME_MAX];

	sprintf(name, "%s%04d%03d%02d%02d%02d%s",
		nameBase, tt->tm_year, tt->tm_yday, tt->tm_hour, tt->tm_min, tt->tm_sec, nameExt);

	printf("saving framegrab as '%s'\n", name);

	FILE* f = fopen(name, "wb");

	if (f)
	{
		const unsigned dims[] = { window_h, window_w };

		fwrite(dims, sizeof(dims), 1, f);
		fwrite(pixels, pixels_len, 1, f);

		fclose(f);
	}

	free(pixels);
}


static std::vector< rend::MeshIndexed >	meshes;

static const rend::ivect3 repeat_grid_side_log2(0, 0, 0);
static const rend::ivect3 repeat_grid_side(
	1 << repeat_grid_side_log2[0],
	1 << repeat_grid_side_log2[1],
	1 << repeat_grid_side_log2[2]);

static const rend::ivect3 repeat_grid_side_shift(
	0,
	repeat_grid_side_log2[0],
	repeat_grid_side_log2[0] + repeat_grid_side_log2[1]);

static const rend::ivect3 repeat_grid_side_mask(
	repeat_grid_side[0] - 1,
	repeat_grid_side[1] - 1,
	repeat_grid_side[2] - 1);

static const unsigned repeat_meshes = repeat_grid_side[0] * repeat_grid_side[1] * repeat_grid_side[2];
static rend::vect3 repeat_spacing(64.f, 64.f, 64.f);

static const unsigned material_palette = 11;
static unsigned curr_mat_idx;

static rend::matx4* repeat_world;
static rend::Material (* repeat_mat)[material_palette];
static rend::Material::EnvMat4::Register* repeat_reg_mat4;

struct BoxAndMaterial
{
	rend::BoxVisual		vis;
	rend::Material		mtr;
};

static std::vector< BoxAndMaterial >	bboxen;

static rend::Renderer		renderer;
static rend::Camera			camA, camB;
static rend::Viewport		vportA, vportB;
static rend::CameraVisual	camVisual;
static rend::Material		camMat;

static GLenum		cull = GL_CCW;
static bool			discardSmooth;
static bool			otherUp;
static rend::vect3	offset;

static rend::Material::EnvMat4::Register	aux_reg_mat4(1);
static rend::Material::EnvVec4::Register	aux_reg_vec4(2);

static rend::Material::EnvMat4::Register*	bbox_reg_mat4;
static rend::Material::EnvVec4::Register	bbox_reg_vec4(1);

static rend::Material::EnvMat4::Register	cam_reg_mat4(1);
static rend::Material::EnvVec4::Register	cam_reg_vec4(1);

static rend::Light	multiLight[3];

static rend::Bone					bone[rend::Material::EnvMat4v::ARRAY_CAPACITY];
static std::vector< rend::Track >	skeletal_animation;


static void
reshape(
#if		REND_TARGET_OS == __TARGET_OS_OSX
	int w, int h)
#else
	GLsizei w, GLsizei h)
#endif
{
	glViewport(0, 0, w, h);
}


static bool
initResources()
{
	// reporting ICD info always comes useful
	const GLubyte* str_version	= glGetString(GL_VERSION);
	const GLubyte* str_vendor	= glGetString(GL_VENDOR);
	const GLubyte* str_renderer	= glGetString(GL_RENDERER);

	printf("gl version, vendor, renderer:\n\t%s\n\t%s\n\t%s\n",
		str_version, str_vendor, str_renderer);

	GLint params[2]; // we won't need more than 2

	glGetIntegerv(GL_MAX_TEXTURE_SIZE, params);
	fprintf(stdout, "GL_MAX_TEXTURE_SIZE: %d\n", params[0]);

	glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, params);
	fprintf(stdout, "GL_MAX_CUBE_MAP_TEXTURE_SIZE: %d\n", params[0]);

	glGetIntegerv(GL_MAX_VIEWPORT_DIMS, params);
	fprintf(stdout, "GL_MAX_VIEWPORT_DIMS: %d, %d\n", params[0], params[1]);

	// vertex program limits
	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_INSTRUCTIONS_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_INSTRUCTIONS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_LOCAL_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_LOCAL_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_ENV_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_ENV_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_TEMPORARIES_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_TEMPORARIES_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_ATTRIBS_ARB, params);
	fprintf(stdout, "GL_VERTEX_PROGRAM_ARB, GL_MAX_PROGRAM_ATTRIBS_ARB: %d\n", params[0]);

#if	!defined(USE_ATI_FRAGMENT_SHADER)

	// fragment program limits (not applicable for ATI_text_fragment_shader)
	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_INSTRUCTIONS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_INSTRUCTIONS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_TEX_INSTRUCTIONS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_TEX_INSTRUCTIONS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_LOCAL_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_LOCAL_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_ENV_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_ENV_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_TEMPORARIES_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_TEMPORARIES_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_PARAMETERS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_PARAMETERS_ARB: %d\n", params[0]);

	glGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_ATTRIBS_ARB, params);
	fprintf(stdout, "GL_FRAGMENT_PROGRAM_ARB, GL_MAX_PROGRAM_ATTRIBS_ARB: %d\n", params[0]);

#endif // USE_ATI_FRAGMENT_SHADER

	// set up ogl things which are not controlled by our framework
	glEnable(GL_VERTEX_PROGRAM_ARB);
	glEnable(A_GL_FRAGMENT_PROGRAM);

	glEnable(GL_DEPTH_TEST);

	glEnable(GL_CULL_FACE);
	glFrontFace(cull);

	// accelerate meshes' first use by explicit pre-uploading to server memory
	std::vector< rend::MeshIndexed >::iterator i = meshes.begin();
	std::vector< rend::MeshIndexed >::iterator iend = meshes.end();

	for (; i != iend; ++i)
		i->upload();

	// set up material palette for each mesh repetition
	repeat_world = (rend::matx4*) malloc(sizeof(*repeat_world) * repeat_meshes);
	repeat_mat = (rend::Material (*)[material_palette]) malloc(sizeof(*repeat_mat) * repeat_meshes);
	repeat_reg_mat4 = (rend::Material::EnvMat4::Register*) malloc(sizeof(*repeat_reg_mat4) * repeat_meshes);

	for (unsigned i = 0; i < repeat_meshes; ++i)
	{
		::new (repeat_mat + i) rend::Material[material_palette];
		::new (repeat_reg_mat4 + i) rend::Material::EnvMat4::Register(1);

		repeat_mat[i][0].setNumPasses(1);
		repeat_mat[i][0].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][0].getPass(0).setNameFragProg("normal_direction_renorm.atifp");
#else
		repeat_mat[i][0].getPass(0).setNameFragProg("normal_direction_renorm.arbfp");
#endif
		repeat_mat[i][0].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][1].setNumPasses(1);
		repeat_mat[i][1].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][1].getPass(0).setNameFragProg("normal_direction.atifp");
#else
		repeat_mat[i][1].getPass(0).setNameFragProg("normal_direction.arbfp");
#endif
		repeat_mat[i][1].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][2].setNumPasses(1);
		repeat_mat[i][2].getPass(0).setNameVertProg("plain_mvp_tex.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][2].getPass(0).setNameFragProg("plain_tex.atifp");
#else
		repeat_mat[i][2].getPass(0).setNameFragProg("plain_tex.arbfp");
#endif
		repeat_mat[i][2].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][3].setNumPasses(1);
		repeat_mat[i][3].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][3].getPass(0).setNameFragProg("normal_length_renorm.atifp");
#else
		repeat_mat[i][3].getPass(0).setNameFragProg("normal_length_renorm.arbfp");
#endif
		repeat_mat[i][3].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][4].setNumPasses(1);
		repeat_mat[i][4].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][4].getPass(0).setNameFragProg("normal_length.atifp");
#else
		repeat_mat[i][4].getPass(0).setNameFragProg("normal_length.arbfp");
#endif
		repeat_mat[i][4].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][5].setNumPasses(1);
		repeat_mat[i][5].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][5].getPass(0).setNameFragProg("phong_blinn_renorm.atifp");
#else
		repeat_mat[i][5].getPass(0).setNameFragProg("phong_blinn_renorm.arbfp");
#endif
		repeat_mat[i][5].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][6].setNumPasses(1);
		repeat_mat[i][6].getPass(0).setNameVertProg("phong_blinn.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][6].getPass(0).setNameFragProg("phong_blinn.atifp");
#else
		repeat_mat[i][6].getPass(0).setNameFragProg("phong_blinn.arbfp");
#endif
		repeat_mat[i][6].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][7].setNumPasses(1);
		repeat_mat[i][7].getPass(0).setNameVertProg("phong_blinn_tex.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][7].getPass(0).setNameFragProg("phong_blinn_tex_renorm.atifp");
#else
		repeat_mat[i][7].getPass(0).setNameFragProg("phong_blinn_tex_renorm.arbfp");
#endif
		repeat_mat[i][7].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][8].setNumPasses(1);
		repeat_mat[i][8].getPass(0).setNameVertProg("phong_blinn_tex.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][8].getPass(0).setNameFragProg("phong_blinn_tex.atifp");
#else
		repeat_mat[i][8].getPass(0).setNameFragProg("phong_blinn_tex.arbfp");
#endif
		repeat_mat[i][8].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][9].setNumPasses(1);
		repeat_mat[i][9].getPass(0).setNameVertProg("phong_blinn_skinning.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][9].getPass(0).setNameFragProg("phong_blinn_renorm.atifp");
#else
		repeat_mat[i][9].getPass(0).setNameFragProg("phong_blinn_renorm.arbfp");
#endif
		repeat_mat[i][9].insertMat4Register(&repeat_reg_mat4[i], 0);

		repeat_mat[i][10].setNumPasses(1);
		repeat_mat[i][10].getPass(0).setNameVertProg("phong_blinn_skinning_morph.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		repeat_mat[i][10].getPass(0).setNameFragProg("normal_direction_renorm.atifp");
#else
		repeat_mat[i][10].getPass(0).setNameFragProg("normal_direction_renorm.arbfp");
#endif
		repeat_mat[i][10].insertMat4Register(&repeat_reg_mat4[i], 0);
	}

	// set up bbox gizmos, one for each mesh
	bboxen.reserve(meshes.size());

	bbox_reg_mat4 = (rend::Material::EnvMat4::Register*) malloc(sizeof(rend::Material::EnvMat4::Register) * meshes.size());

	for (unsigned i = 0; i < meshes.size(); ++i)
	{
		::new (bbox_reg_mat4 + i) rend::Material::EnvMat4::Register(1);

		bboxen.push_back(BoxAndMaterial());

		BoxAndMaterial& bnm = bboxen.back();
		bnm.mtr.setNumPasses(1);
		bnm.mtr.getPass(0).setNameVertProg("plain_mvp.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
		bnm.mtr.getPass(0).setNameFragProg("plain_color.atifp");
#else
		bnm.mtr.getPass(0).setNameFragProg("plain_color.arbfp");
#endif
		bnm.mtr.insertMat4Register(&bbox_reg_mat4[i], 0);
		bnm.mtr.insertVec4Register(&bbox_reg_vec4, 0);
	}

	rend::Material::EnvVec4* reg_color_box = bbox_reg_vec4.insert("color");
	reg_color_box->datum = rend::ColorA(1.f, 0.f, 0.f);

	// set up material for camera gizmo
	camMat.setNumPasses(1);
	camMat.getPass(0).setNameVertProg("plain_mvp.arbvp");
#ifdef USE_ATI_FRAGMENT_SHADER
	camMat.getPass(0).setNameFragProg("plain_color.atifp");
#else
	camMat.getPass(0).setNameFragProg("plain_color.arbfp");
#endif
	camMat.insertMat4Register(&cam_reg_mat4, 0);
	camMat.insertVec4Register(&cam_reg_vec4, 0);

	rend::Material::EnvVec4* reg_color_cam = cam_reg_vec4.insert("color");
	reg_color_cam->datum = rend::ColorA(0.f, 1.f, 1.f);

	// set up primary camera and a pair of viewports
	camA.setProjectionType(rend::Camera::PROJECTION_PERSPECTIVE);
	camA.setFovX(M_PI_4 * .75f);
	camA.setFovY(M_PI_4 * .75f);
	camA.setNearClipDistance(4.f);
	camA.setFarClipDistance(1024.f);
	camA.setExtentX(camA.getExtentX() * ((float) window_w / (float) window_h));

	vportA.setClearColor(rend::ColorA(.25, .25, .25));

	vportB.setClearColor(rend::ColorA(.125, .625, .75));
	vportB.setRect(rend::Box2(rend::vect2(0, 0), rend::vect2(.25, .25)));

	renderer.setFrameDims(window_w, window_h);

	// read in optional skeleton snapshot file
	FILE* f = fopen("assets/bone_array.bin", "rb");

	if (f)
	{
		rend::Material::EnvMat4v* reg_bone = rend::Material::EnvMat4v::s_default_register.insert(rend::Material::sn_mat4v_bone);
		reg_bone->count = fread(reg_bone->array, sizeof(reg_bone->array[0]), reg_bone->ARRAY_CAPACITY, f);
		fclose(f);

		for (unsigned i = 0; i < reg_bone->count; ++i)
			for (unsigned j = 0; j < sizeof(reg_bone->array[0]) / sizeof(reg_bone->array[0][0][0]); ++j)
				rend::swapEndiannessIfHostBig(reg_bone->array[i].dekastF()[j]);

		fprintf(stdout, "read skeleton snapshot file of %u bones\n", reg_bone->count);
	}

	// read optional skeleton file (overrides skeleton snapshot)
	f = fopen("assets/Ahmed_GEO.skeleton", "rb");

	if (f)
	{
		rend::Material::EnvMat4v* reg_bone = rend::Material::EnvMat4v::s_default_register.insert(rend::Material::sn_mat4v_bone);

		rend::loadSkeletonAnimationAge(reg_bone, bone, skeletal_animation, f);

		fclose(f);
	}

	// set up secondary camera and produce a visual from that
	camB.setExtentX(16.f * ((float) window_w / (float) window_h));
	camB.setExtentY(16.f);
	camB.setNearClipDistance(1.f);
	camB.setFarClipDistance(48.f);

	camVisual.buildFrom(camB);

	rend::Material::Pass::createNormalizerCubemap(
		&rend::Material::Texture::s_default_register,
		rend::Material::sn_tex_normalizer_cubemap);

	rend::Material::Pass::createExponentiationMap(
		&rend::Material::Texture::s_default_register,
		rend::Material::sn_tex_exponentiation_map,
		max_shininess);

	multiLight[0].setPosition(rend::vect4(0.f, 0.f, 1.f, 0.f));
	multiLight[0].setDiffuse(rend::ColorA(1.f, 1.f, 1.f));
	multiLight[0].setSpecular(rend::ColorA(1.f, 1.f, 1.f));
	multiLight[1].setPosition(rend::vect4(-1.f, 0.f, 0.f, 0.f));
	multiLight[1].setDiffuse(rend::ColorA(1.f, 0.f, 0.f));
	multiLight[1].setSpecular(rend::ColorA(1.f, 1.f, 0.f));
	multiLight[2].setPosition(rend::vect4(1.f, 0.f, 0.f, 0.f));
	multiLight[2].setDiffuse(rend::ColorA(0.f, 0.f, 1.f));
	multiLight[2].setSpecular(rend::ColorA(0.f, 1.f, 1.f));

	// set up Phong illumination model params
	const rend::ColorA Acs(.2f, .2f, .2f);

	const rend::ColorA Ecm(0.f, .25f, 0.f);
	const rend::ColorA Acm(.2f, .2f, .2f);
	const rend::ColorA Dcm(1.f, .5f, .3f);
	const rend::ColorA Scm(.25f, .25f, .25f);
	const float Srm(24.f);

	rend::Material::EnvVec4* reg_shininess = rend::Material::EnvVec4::s_default_register.insert("material.shininess");
	reg_shininess->datum = rend::vect4(Srm * (1.f / max_shininess), 0.f, 0.f, 0.f);

	rend::vect4 lmprod;
	lmprod.mul(Acm, Acs);
	lmprod.add(Ecm);

	rend::Material::EnvVec4* reg_lmprod = rend::Material::EnvVec4::s_default_register.insert("lightModelProduct.sceneColor");
	reg_lmprod->datum = lmprod; // Ecm + Acm * Acs

	for (unsigned i = 0; i < sizeof(multiLight) / sizeof(multiLight[0]); ++i)
	{
		char buffer[32];
		sprintf(buffer, "lightProduct[%u].", i);

		const std::string prod_idx(buffer);

		rend::Material::EnvVec4* reg_prod_a = rend::Material::EnvVec4::s_default_register.insert(prod_idx + "ambient");
		reg_prod_a->datum.mul(Acm, multiLight[i].getAmbient());		// Acm * Acli

		rend::Material::EnvVec4* reg_prod_d = rend::Material::EnvVec4::s_default_register.insert(prod_idx + "diffuse");
		reg_prod_d->datum.mul(Dcm, multiLight[i].getDiffuse());		// Dcm * Dcli

		rend::Material::EnvVec4* reg_prod_s = rend::Material::EnvVec4::s_default_register.insert(prod_idx + "specular");
		reg_prod_s->datum.mul(Scm, multiLight[i].getSpecular());	// Scm * Scli
	}

	// set up a default albedo texture
	rend::Material::Pass::createTexture2DFromFile(
		&rend::Material::Texture::s_default_register,
		"albedo",
		"assets/texture.raw");

	return !rend::reportGLError();
}


static void
display()
{
	static bool once = true;
	if (once)
	{
		initResources();
		once = false;
	}

	// time this frame
	static unsigned long long t0 = 0;
	const unsigned long long t1 = rend::timer();

	// set up dynamic rendering controllers
	rend::matx4 world;

	if (otherUp)
		world.rotate(-M_PI_2, 1.f, 0.f, 0.f);
	else
		world.identity();

	world.setColS(3, offset);

	static float angle = 0;
	static float angle_time = 0;
	static float anim_time = 0;

	const rend::vect3 pos(::sinf(angle), 0.f, ::cosf(angle));

	const float cos_angle_time = ::cosf(angle_time) * .5f + .5f;

	if (t0)
	{
		angle = ::fmodf(angle + angle_step * (t1 - t0) * rfreq, 2.f * M_PI);
		angle_time = ::fmodf(angle_time + angle_step * 16.0 * (t1 - t0) * rfreq, 2.f * M_PI);

		anim_time += anim_speed * (t1 - t0) * rfreq;
		if (anim_time > 1.f)
		{
			anim_time -= ::floorf(anim_time);
			rend::resetSkeletonAnimProgress(skeletal_animation);
		}
	}

	rend::vect3 roll;
	roll.cross(pos, rend::vect3(0, 1, 0));

	camA.setPosition(rend::vect3(pos[0] * radius, cameraY, pos[2] * radius));
	camA.setTarget(rend::vect3(0.f, targetY, 0.f));

	const rend::vect3 &sub = rend::vect3().sub(camA.getPosition(), camA.getTarget());
	roll.cross(sub);
	camA.setRoll(roll);

	const float epsilon = 1e-6;
	assert(camA.getRoll().dp(camA.getDirection()) <= epsilon);

	// shader parameters, view A
	{
		rend::matx4 mv;
		mv.mul(camA.getInverseWorldTransform(), world);

		rend::matx4 mvi;
		const bool invertible = mvi.invert(mv);
		assert(invertible);

		rend::Material::EnvVec4v* reg_lp_obj = rend::Material::EnvVec4v::s_default_register.insert(rend::Material::sn_vec4v_lp_obj);

		const unsigned light_count = sizeof(multiLight) / sizeof(multiLight[0]);
		assert(rend::Material::EnvVec4v::ARRAY_CAPACITY >= light_count);

		reg_lp_obj->count = light_count;

		for (unsigned i = 0; i < light_count; ++i)
			mvi.transform(multiLight[i].getPosition(), reg_lp_obj->array[i]);

		rend::Material::EnvVec4* reg_lp_obj0 = rend::Material::EnvVec4::s_default_register.insert(rend::Material::sn_vec4_lp_obj_0);
		reg_lp_obj0->datum = reg_lp_obj->array[0];

		rend::Material::EnvVec4* reg_vp_obj = rend::Material::EnvVec4::s_default_register.insert(rend::Material::sn_vec4_vp_obj);
		const bool local_viewer = true;

		if (local_viewer)
			mvi.getCol(3, reg_vp_obj->datum);
		else
			mvi.getCol(2, reg_vp_obj->datum);

		rend::matx4 mvp;
		mvp.mul(camA.getProjection(), mv);

/*		rend::Material::EnvMat4* reg_mvp = rend::Material::EnvMat4::s_default_register.insert(rend::Material::sn_mat4_mvp);
		reg_mvp->datum = mvp;
*/
		rend::Material::EnvVec4* reg_angle_time = rend::Material::EnvVec4::s_default_register.insert(rend::Material::sn_vec4_angular_time);
		reg_angle_time->datum = rend::vect4(angle_time, cos_angle_time, 0.f, 0.f);

		for (unsigned r = 0; r < repeat_meshes; ++r)
		{
			rend::Material::EnvMat4* repeat_mvp = repeat_reg_mat4[r].insert(rend::Material::sn_mat4_mvp);
			repeat_mvp->datum.translate(
				((int(r >> repeat_grid_side_shift[0]) & repeat_grid_side_mask[0]) - repeat_grid_side[0] / 2) * repeat_spacing[0],
				((int(r >> repeat_grid_side_shift[1]) & repeat_grid_side_mask[1]) - repeat_grid_side[1] / 2) * repeat_spacing[1],
				((int(r >> repeat_grid_side_shift[2]) & repeat_grid_side_mask[2]) - repeat_grid_side[2] / 2) * repeat_spacing[2]);

			repeat_world[r].mul(world, repeat_mvp->datum);

			repeat_mvp->datum.mul(mvp);
		}

		// animate the skeleton, if one exists
		rend::Material::EnvMat4v* reg_bone = rend::Material::EnvMat4v::s_default_register.insert(rend::Material::sn_mat4v_bone);

		rend::animateSkeleton(reg_bone, bone, skeletal_animation, anim_time);
	}
	// shader parameters, view B
	{
		rend::matx4 mv;
		mv.mul(camB.getInverseWorldTransform(), world);

		rend::matx4 mvi;
		const bool invertible = mvi.invert(mv);
		assert(invertible);

		rend::vect4 lp_obj;
		mvi.transform(multiLight[0].getPosition(), lp_obj);

		rend::Material::EnvVec4* reg_lp_obj = aux_reg_vec4.insert(rend::Material::sn_vec4_lp_obj_0);
		reg_lp_obj->datum = lp_obj;

		rend::Material::EnvVec4* reg_vp_obj = aux_reg_vec4.insert(rend::Material::sn_vec4_vp_obj);
		const bool local_viewer = false;

		if (local_viewer)
			mvi.getCol(3, reg_vp_obj->datum);
		else
			mvi.getCol(2, reg_vp_obj->datum);

		rend::matx4 mvp;
		mvp.mul(camB.getProjection(), mv);

		rend::Material::EnvMat4* reg_mvp = aux_reg_mat4.insert(rend::Material::sn_mat4_mvp);
		reg_mvp->datum = mvp;
	}

	// render primary view
	renderer.setViewport(&vportA);

	renderer.openGroup(&camA, true);
	renderer.openGroup(&camA, false);

	std::vector< rend::MeshIndexed >::iterator i = meshes.begin();
	std::vector< rend::MeshIndexed >::iterator iend = meshes.end();

	std::vector< BoxAndMaterial >::iterator bi = bboxen.begin();

	if (repeat_meshes > 1)
	{

#if defined(__DONT_CLIP__)

		// draw each mesh the specified number of repetitions
		for (; i != iend; ++i)
			for (unsigned r = 0; r < repeat_meshes; ++r)
				renderer.queueDraw(&*i, &repeat_mat[r][curr_mat_idx], repeat_world[r]);

#else // __DONT_CLIP__

		// draw only those mesh repetitions seen by the secondary camera
		for (; i != iend; ++i)
		{
			const rend::Box3& bbox = i->getBBox();

			if (!bbox.isValid())
				continue;

			for (unsigned r = 0; r < repeat_meshes; ++r)
			{
				rend::Frustum frustB;

				if (camB.getFrustum(frustB, repeat_world[r]))
					if (frustB.isAABoxIntersecting(bbox.getMinimum(), bbox.getMaximum()))
						renderer.queueDraw(&*i, &repeat_mat[r][curr_mat_idx], repeat_world[r]);
			}
		}

#endif // __DONT_CLIP__

	}
	else
	{

#if defined(__DONT_CLIP__)

		// draw every mesh
		for (; i != iend; ++i, ++bi)
		{
			renderer.queueDraw(&*i, &repeat_mat[0][curr_mat_idx], world);

			assert(bi != bboxen.end());
			bi->vis.setBBox(i->getBBox());
		}

#else // __DONT_CLIP__

		// draw only those meshes seen by the secondary camera
		for (; i != iend; ++i, ++bi)
		{
			const rend::Box3& bbox = i->getBBox();

			if (!bbox.isValid())
				continue;

			assert(bi != bboxen.end());
			bi->vis.setBBox(rend::Box3(
				 FLT_MAX,  FLT_MAX,  FLT_MAX,
				-FLT_MAX, -FLT_MAX, -FLT_MAX ));

			rend::Frustum frustB;

			if (camB.getFrustum(frustB, world))
				if (frustB.isAABoxIntersecting(bbox.getMinimum(), bbox.getMaximum()))
				{
					renderer.queueDraw(&*i, &repeat_mat[0][curr_mat_idx], world);

					bi->vis.setBBox(i->getBBox());
				}
		}

#endif // __DONT_CLIP__

	}

#if !defined(__DONT_CLIP__)

	// draw secondary camera's gizmo
	rend::Material::EnvMat4* reg_mvp = cam_reg_mat4.insert(rend::Material::sn_mat4_mvp);
	reg_mvp->datum.mul(camA.getInverseWorldTransform(), camB.getWorldTransform());
	reg_mvp->datum.mul(camA.getProjection());

	renderer.queueDraw(&camVisual, &camMat, camB.getWorldTransform());

#endif // __DONT_CLIP__

	renderer.closeGroup();

	// draw mesh bboxen
	for (bi = bboxen.begin(); bi != bboxen.end(); ++bi)
	{
		const rend::Box3& bbox = bi->vis.getBBox();

		if (bbox.isValid())
		{
			rend::vect3 s, t;

			bbox.getSpan(s);
			bbox.getCentre(t);
			s.mul(.5f);

			rend::matx4 mvp;
			mvp.scale(s[0], s[1], s[2]);
			mvp.setColS(3, t);

			mvp.mul(world);
			mvp.mul(camA.getInverseWorldTransform());
			mvp.mul(camA.getProjection());

			rend::Material::EnvMat4* reg_mvp = bi->mtr.getFirstMat4Register()->insert(rend::Material::sn_mat4_mvp);
			reg_mvp->datum = mvp;

			renderer.queueDraw(&bi->vis, &bi->mtr, world);
		}
	}

	renderer.closeGroup();

	renderer.flush();

#if !defined(__DONT_CLIP__)

	if (mini_view && repeat_meshes == 1)
	{
		// render secondary view
		renderer.setViewport(&vportB);

		renderer.openGroup(&camB, false);

		std::vector< rend::MeshIndexed >::iterator j = meshes.begin();
		std::vector< rend::MeshIndexed >::iterator jend = meshes.end();

		const unsigned mi = 5;
		assert(mi < material_palette);

		for (; j != jend; ++j)
			renderer.queueDraw(&*j, &repeat_mat[0][mi], world);

		renderer.closeGroup();

		repeat_mat[0][mi].insertMat4Register(&aux_reg_mat4, 0);
		repeat_mat[0][mi].insertVec4Register(&aux_reg_vec4, 0);

		renderer.flush();

		repeat_mat[0][mi].removeMat4Register(&aux_reg_mat4);
		repeat_mat[0][mi].removeVec4Register(&aux_reg_vec4);
	}

#endif // __DONT_CLIP__

	if (rend::reportGLError(stderr))
	{
		printf("frame aborted\n");
		return;
	}

	const unsigned periodLength = 100;
	static unsigned periodCount = periodLength - 1;

	static unsigned long long acct = 0;

	if (t0)
	{
		acct += t1 - t0;

		if (!--periodCount)
		{
			const double sec = double(acct) / (double(periodLength) * freq);

			printf("time: %f s/frame, %f FPS\n", sec, 1.0 / sec);

			periodCount = periodLength;
			acct = 0;
		}
	}

	t0 = t1;

	if (grab_frame)
	{
		saveFramebuffer();
		grab_frame = false;
	}

	if (0 == --frames)
		exit(0);

	glutSwapBuffers();
	glutPostRedisplay();
}


static void
kbd(unsigned char key, int x, int y)
{
	rend::vect3 pos;

	switch (key)
	{
	case 'z':
	case 'Z': // raise primary camera's position
		cameraY += rateY;
		break;
	case 'x':
	case 'X': // lower primary camera's position
		cameraY -= rateY;
		break;
	case 'a':
	case 'A': // raise primary camera's aim
		targetY += rateY;
		break;
	case 's':
	case 'S': // lower primary camera's aim
		targetY -= rateY;
		break;
	case 'q':
	case 'Q':
		radius -= radius > 1.f ? radius * .5f : 0.f;
		break;
	case 'w':
	case 'W':
		radius += radius < 1024.f ? radius : 0.f;
		break;
	case ' ': // toggle primary camera's spinning around y
		*reinterpret_cast< rend::int32* >(&angle_step) ^= *reinterpret_cast< const rend::int32* >(&rateA);
		break;
	case '\r': // take a snapshot of the framebuffer
		grab_frame = true;
		break;
	case '4': // move secondary camera along -x
		pos.add(camB.getPosition(), rend::vect3(-1, 0, 0)); 
		camB.setPosition(pos);
		break;
	case '6': // move secondary camera along +x
		pos.add(camB.getPosition(), rend::vect3(1, 0, 0)); 
		camB.setPosition(pos);
		break;
	case '2': // move secondary camera along -y
		pos.add(camB.getPosition(), rend::vect3(0, -1, 0)); 
		camB.setPosition(pos);
		break;
	case '8': // move secondary camera along +y
		pos.add(camB.getPosition(), rend::vect3(0, 1, 0)); 
		camB.setPosition(pos);
		break;
	case '5': // move secondary camera along -z
		pos.add(camB.getPosition(), rend::vect3(0, 0, -1)); 
		camB.setPosition(pos);
		break;
	case '0': // move secondary camera along +z
		pos.add(camB.getPosition(), rend::vect3(0, 0, 1)); 
		camB.setPosition(pos);
		break;
	case 'i':
	case 'I': // switch viewA to using the next material from the material palette
		++curr_mat_idx;
		if (curr_mat_idx == material_palette)
			curr_mat_idx = 0;
		break;
	case 'o':
	case 'O': // switch viewA to using the previous material from the material palette
		if (curr_mat_idx == 0)
			curr_mat_idx = material_palette;
		--curr_mat_idx;
		break;
	case '`': // toggle culling between CCW and CW
		if (cull == GL_CCW)
			cull = GL_CW;
		else
			cull = GL_CCW;
		glFrontFace(cull);
		break;
	case '-':
		camB.setExtentX(camB.getExtentX() * .5f);
		camB.setExtentY(camB.getExtentY() * .5f);
		camVisual.buildFrom(camB);
		break;
	case '+':
		camB.setExtentX(camB.getExtentX() * 2.f);
		camB.setExtentY(camB.getExtentY() * 2.f);
		camVisual.buildFrom(camB);
		break;
	case '/':
		camB.setFarClipDistance(camB.getFarClipDistance() * .5f);
		camVisual.buildFrom(camB);
		break;
	case '*':
		camB.setFarClipDistance(camB.getFarClipDistance() * 2.f);
		camVisual.buildFrom(camB);
		break;
	case '.':
		rend::MeshIndexed::save("out.mesh", meshes);
		break;
	case ',':
		rend::MeshIndexed::save_texcoord("out.mesh2", meshes);
		break;
	}
}


static bool
isFilenameExt(const char *filename, const char *ext)
{
	const size_t extLen = strlen(ext);
	const size_t filenameLen = strlen(filename);

	return filenameLen > extLen
		? !strcasecmp(filename + filenameLen - extLen, ext)
		: false;
}


static const char*
validate_fullscreen(const char* string)
{
	if (!string)
		return NULL;

	unsigned x, y, bpp, hz;

	if (4 != sscanf(string, "%ux%u:%2u@%u", &x, &y, &bpp, &hz))
		return NULL;

	if (!x || !y || (bpp != 16 && bpp != 32) || !hz)
		return NULL;

	window_w = x;
	window_h = y;

	return string;
}


static void
handler_exit()
{
	rend::Material::Program::s_default_register.discard(0);
	rend::Material::Texture::s_default_register.discard(0);
	rend::AttrBuffer16::s_default_register.discard(0);

	for (unsigned i = 0; i < meshes.size(); ++i)
		(bbox_reg_mat4 + i)->rend::Material::EnvMat4::Register::~Register();

	free(bbox_reg_mat4);

	for (unsigned i = 0; i < repeat_meshes; ++i)
	{
		for (unsigned j = 0; j < material_palette; ++j)
			repeat_mat[i][j].rend::Material::~Material();

		repeat_reg_mat4[i].rend::Material::EnvMat4::Register::~Register();
	}

	free(repeat_mat);
	free(repeat_reg_mat4);
	free(repeat_world);
}


int
main(int argc, char **argv)
{
	const char *filename = NULL;
	bool cli_err = false;

	for (int i = 1; i < argc && !cli_err; ++i)
	{
		if (argEscapePrefix != argv[i][0])
		{
			if (!filename)
			{
				filename = argv[i];
				continue;
			}
			cli_err = true;
			continue;
		}

		if (!strcmp(argv[i] + 1, argFrequency))
		{
			if (!(++i < argc) || (1 != sscanf(argv[i], "%f", &freq)) || !(freq > 0.f))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + 1, argNumFrames))
		{
			if (!(++i < argc) || (1 != sscanf(argv[i], "%u", &frames)))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + 1, argFullscreen))
		{
			if (!(++i < argc && (fullscreen = validate_fullscreen(argv[i]))))
				cli_err = true;

			continue;
		}

		if (!strcmp(argv[i] + 1, argDiscardSmooth))
		{
			discardSmooth = true;
			continue;
		}

		if (!strcmp(argv[i] + 1, argInvertCull))
		{
			cull = GL_CW;
			continue;
		}

		if (!strcmp(argv[i] + 1, argOtherUp))
		{
			otherUp = true;
			continue;
		}

		if (!strcmp(argv[i] + 1, argNoMiniView))
		{
			mini_view = false;
			continue;
		}

		if (!strcmp(argv[i] + 1, argModelOffset))
		{
			float x, y, z;

			if (++i < argc - 2 &&
				1 == sscanf(argv[i],   "%f", &x) &&
				1 == sscanf(argv[++i], "%f", &y) &&
				1 == sscanf(argv[++i], "%f", &z))
			{
				offset = rend::vect3(x, y, z);
			}
			else
				cli_err = true;

			continue;
		}

		cli_err = true;
	}

	if (cli_err || !filename)
	{
		printf("usage: testbed [<option> ...] <mesh_file>\n"
			   "options:\n"
			   "\t-%s <Hz>\t\t\t: override autodetection of the high-resolution timer's frequency\n"
			   "\t-%s <N>\t\t\t\t: run for N frames then terminate\n"
			   "\t-%s\t\t\t\t: suppress the secondary (mini) view, otherwise showing a feed from the camera gizmo\n"
			   "\t-%s\t\t\t: discard mesh-supplied smoothing-group information; can help with the loading of some 3ds meshes\n"
			   "\t-%s\t\t\t\t: invert culling; CW geometry is culled by default\n"
			   "\t-%s\t\t\t\t: use +z as the up direction of the model; default is +y\n"
			   "\t-%s <X> <Y> <Z>\t\t: apply offset to model\n"
			   "\t-%s <width>x<height>:<depth>@<Hz>\t: set fullscreen output; uses GLUT control syntax\n",
			argFrequency,
			argNumFrames,
			argNoMiniView,
			argDiscardSmooth,
			argInvertCull,
			argOtherUp,
			argModelOffset,
			argFullscreen);

		return -1;
	}

	if (!freq)
		freq = rend::timer_freq();

	rfreq = 1.f / freq;

	printf("timer frequency %.3f MHz\nloading mesh...\n", freq / 1e6);

	bool (* load)(
		const std::string&,
		std::vector< rend::MeshIndexed >&,
		rend::AttrBuffer16::Register*);

	if (isFilenameExt(filename, ".3ds"))
		load = discardSmooth
			? rend::MeshIndexed::load3ds_nosmooth
			: rend::MeshIndexed::load3ds_smooth;
	else
	if (isFilenameExt(filename, ".age"))
		load = rend::MeshIndexed::loadAge;
	else
		load = rend::MeshIndexed::load;

	// make sure loading does not exceed our memory or attribute buffer capacity
	try {

		if (!load(filename, meshes, &rend::AttrBuffer16::s_default_register))
		{
			printf("failed loading from file '%s'; abort\n", filename);
			return -1;
		}
	}
	catch (const std::bad_alloc&)
	{
		printf("insufficient memory or too many meshes in file '%s' (allowed %d); abort\n", filename, rend::AttrBuffer16::s_default_register.capacity() / 2);
		return -1;
	}

	unsigned totalVerts	= 0;
	unsigned totalTris	= 0;

	std::vector< rend::MeshIndexed >::const_iterator it = meshes.begin();
	for (; it != meshes.end(); ++it)
	{
		totalVerts	+= it->getNumVertices();
		totalTris	+= it->getNumTriangles();
	}

	printf("total triangles: %d\ntotal vertices: %d\n", totalTris, totalVerts);

	// set the opengl system; initialize glut and, on select platforms, glew
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);

	if (fullscreen)
	{
		glutGameModeString(fullscreen);
		glutEnterGameMode();
	}
	else
	{
		glutInitWindowPosition(0, 0);
		glutInitWindowSize(window_w, window_h);
		glutCreateWindow(argv[0]);
	}

#if     REND_TARGET_OS != __TARGET_OS_OSX

	const GLenum err = glewInit();

	if (GLEW_OK != err)
	{
		fprintf(stderr, "glew error: %s\n", glewGetErrorString(err));
		return -1;
	}

#endif

	glutReshapeFunc(reshape);
	glutDisplayFunc(display);
	glutKeyboardFunc(kbd);

	if (atexit(handler_exit))
	{
		printf("error: could not register an exit routine with atexit()\n");
		return -1;
	}

	glutMainLoop();

	return 0;
}
